## Классные методы (методы класса)

### Пример

```python
class Point:
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

    def hello1():
        print('hello1')

    @staticmethod
    def hello2():
        print('hello2')
    

p1 = Point(2, 7)
p2 = Point(12, 7)


Point.hello1()
f = Point.hello1
f()
p1.hello2()
```


### Статические методы
Кроме обычных методов класс может определять статические методы. Такие методы предваряются аннотацией `@staticmethod` и относятся в целом к классу. Статические методы обычно определяют поведение, которое не зависит от конкретного объекта:



```python
class Person:
    __type = "Person"
 
    @staticmethod
    def print_type():
        print(Person.__type)
 
 
Person.print_type()     # Person - обращение к статическому методу через имя класса
 
tom = Person()
tom.print_type()     # Person - обращение к статическому методу через имя объекта
```

В данном случае в классе Person определен атрибут класса `__type`, который хранит значение, общее для всего класса - название класса. 

Причем поскольку название атрибута предваряется двумя подчеркиваниями, то данный атрибут будет приватным, что защитит от недопустимого изменения.

Также в классе Person определен статический метод print_type, который выводит на консоль значение атрибута `__type`. 

Действие этого метода не зависит от конкретного объекта и относится в целом ко всему классу - вне зависимости от объекта на консоль будет выводится одно и то же значение атрибута `__type. Поэтому такой метод можно сделать статическим.


```python
class ИмяКласса:
    атрибут_класса = 0

    @staticmethod
    def static_method():
        return ИмяКласса.атрибут_класса

    @classmethod
    def class_method(cls):
        return cls.атрибут_класса

объект = ИмяКласса()

объект.static_method()
объект.class_method()
```

Технической между ними разницы нет. 

Есть синтаксическая разница. Если мы используем декоратор `@staticmethod` то обращение идет к классу через точку его метод.

```python
    @staticmethod
    def static_method():
        return Lamp.count

    @classmethod
    def class_method(cls):
        return cls.count


print (lamp1.static_method())
print (lamp1.class_method())
```

Таким образом 
```python
class Lamp:
    brand = "Philips"
    count = 0
    def __init__(self, floor=0):
        self.__state = False
        self.__floor = floor
        Lamp.count +=1
        print(f'Создана лампочка {Lamp.brand} № {Lamp.count}' )
```



Некая переменная в которую прилетает имя класса, по соглашению, по конвенции – название её `cls` ( по аналогии `self`).

В первом случае имя класс мы пишем руками.

Никакого `self`. Но какая возникает проблема.

Подумайте, если мы поменяем имя класса. Что это значит?

Нам нужно будет поменять имя класса во всех статических методах.

В случае с класс-методами – этого делать не нужно.

Разница в терминах ООП никакой. Кроме того, что необходимо менять название в статик-методах.

Ну и, соответственно, внутри класса мы не можем обращаться к каким-либо объектам. Только к классу.

К общему поведению всех объектов.

Т.е класс – это тоже самостоятельная единица.
Иногда класс используется просто как коробка методов
```python
class Math3D:
    PI = 3.14
    @classmethod
    def cos(n):
        pass

    @classmethod
    def sin(n):
        pass

Math3D.cos(2)
```

т.е когда мы будем вызывать методы, без создания экземпляров данного класса.

Там могут просто лежать некие алгоритмы.



## P.S.

На самом деле, в Python можно относительно легко обратиться и к приватным атрибутам извне. Если распечатать все атрибуты экземпляра:

```python
print(dir(lamp1))
```
то среди прочих мы увидим, следующие:

`'_Lamp__floor', '_Lamp__state'`

Это и есть кодовые имена приватных атрибутов, к которым мы можем обратиться через ссылку `lamp1`:

```python
print(lamp1._Lamp__state)
```
и менять их. 

Однако, так делать крайне не рекомендуется и двойное подчеркивание должно сигнализировать программисту, что работать с такими атрибутами нужно только через разрешенные интерфейсные методы.

Иначе, возможны непредвиденные ошибки.

Если у вас появилась необходимость лучше защитить методы класса от доступа извне, то это можно сделать с помощью модуля `accessify`. Для его установки нужно выполнить команду:

pip install accessify

И, затем, импортировать из него два декоратора:
```
from accessify import private, protected
```

Далее, нужный декоратор просто применяем к методу и он становится либо приватным (private), либо защищенным (protected):
```python
    @private
    @classmethod
    def check_value(cls, x):
        return type(x) in (int, float)
```
Все, теперь мы можем обратиться к check_value только внутри класса, но не извне


### Выводы:
* реализация режимов доступа `public, protected и private`,
* правильное обращение  скрытым атрибутам через интерфейсные методы – сеттеры и геттеры.